package yxy.apple.http;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.GZIPOutputStream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CookieStore;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.ssl.SSLContextBuilder;

import yxy.apple.http.request.HTTPRequest;
import yxy.apple.http.response.HTTPResponse;
import yxy.apple.util.http.HTTPProtocol;
import yxy.apple.util.http.HTTPProtocol.ContentEncoding;
import yxy.apple.util.http.HTTPProtocol.HeaderKey;
import yxy.apple.util.http.HTTPProtocol.Method;
import yxy.apple.util.http.HTTPProxy;

public class HTTPConnection {
	protected static final Log logger = LogFactory.getLog(HTTPConnection.class);

	public static interface ResponseReceivable<Request extends HTTPRequest<?>, Response extends HTTPResponse<?>> {
		public void received(HTTPConnection connection, Request request, Response response);

		public void caughtException(HTTPConnection connection, HTTPException e);
	}

	private static final HostnameVerifier Verifier = new HostnameVerifier() {
		@Override
		public boolean verify(String hostname, SSLSession session) {
			return true;
		}
	};

	private static PoolingHttpClientConnectionManager sharedConnectionManager = null;

	private CloseableHttpClient client = null;
	private CookieStore cookieStore = null;
	private HTTPProxy proxy = null;
	private PoolingHttpClientConnectionManager manager = null;

	private static PoolingHttpClientConnectionManager sharedConnectionManager() {
		if (sharedConnectionManager == null) {
			try {
				SSLContextBuilder ssl = new SSLContextBuilder();
				ssl.loadTrustMaterial(null, new TrustStrategy() {
					@Override
					public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
						return true;
					}
				});
				SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(ssl.build(), new String[] { "SSLv3", "TLSv1.2" }, null,
						NoopHostnameVerifier.INSTANCE);
				Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
						.register("http", new PlainConnectionSocketFactory()).register("https", factory).build();
				sharedConnectionManager = new PoolingHttpClientConnectionManager(registry);
				sharedConnectionManager.setDefaultMaxPerRoute(10);
				sharedConnectionManager.setMaxTotal(1024);
			} catch (Exception e) {
				logger.fatal("构造ConnectionManager发生异常", e);
				// throw new HTTPException(e);
			}
		}
		return sharedConnectionManager;

	}

	public HTTPConnection(long timeout) {
		this(timeout, null);
	}

	public HTTPConnection(long timeout, InetAddress localInetAddress) {
		this(timeout, localInetAddress, null);
	}

	public HTTPConnection(long timeout, InetAddress localInetAddress, CookieStore cs) {
		this(timeout, localInetAddress, cs, null);
	}

	public HTTPConnection(long timeout, InetAddress localInetAddress, CookieStore cs, HTTPProxy proxy) {
		this(timeout, localInetAddress, cs, proxy, null, null);
	}

	public HTTPConnection(long timeout, InetAddress localInetAddress, CookieStore cs, HTTPProxy proxy, HttpRequestInterceptor requestInterceptor,
			HttpResponseInterceptor responseInterceptor) {
		this(timeout, localInetAddress, cs, proxy, requestInterceptor, responseInterceptor, Verifier, sharedConnectionManager(), true);
	}

	public HTTPConnection(long timeout, InetAddress localInetAddress, CookieStore cs, HTTPProxy proxy, HttpRequestInterceptor requestInterceptor,
			HttpResponseInterceptor responseInterceptor, HostnameVerifier sslHostnameVerifier, PoolingHttpClientConnectionManager manager,
			boolean managerShared) {

		HttpClientBuilder builder = HttpClientBuilder.create();
		if (sslHostnameVerifier != null) {
			builder.setSSLHostnameVerifier(sslHostnameVerifier);
		}
		if (manager != null) {
			builder.setConnectionManager(manager).setConnectionManagerShared(managerShared);
			this.manager = manager;
		}
		{
			RequestConfig.Builder rcb = RequestConfig.custom();
			rcb.setConnectTimeout((int) timeout);
			rcb.setSocketTimeout((int) timeout);
			if (localInetAddress != null) {
				rcb.setLocalAddress(localInetAddress);
			}
			if (proxy != null) {
				HttpHost host = new HttpHost(proxy.getIPString(), proxy.port());
				rcb.setProxy(host);
				if (proxy.username() != null && proxy.password() != null) {
					CredentialsProvider cp = new BasicCredentialsProvider();
					cp.setCredentials(new AuthScope(proxy.getIPString(), proxy.port()), new UsernamePasswordCredentials(proxy.username(), proxy.password()));
					builder.setDefaultCredentialsProvider(cp);
				}
			}
			builder.setDefaultRequestConfig(rcb.build());
		}
		if (cs == null) {
			this.cookieStore = new BasicCookieStore();
		}
		builder.setDefaultCookieStore(this.cookieStore);
		builder.disableRedirectHandling();
		this.proxy = proxy;
		if (requestInterceptor != null) {
			builder.addInterceptorFirst(requestInterceptor);
		}
		if (responseInterceptor != null) {
			builder.addInterceptorFirst(responseInterceptor);
		}
		this.client = builder.build();
	}

	/**
	 * 发送一个同步的http请求
	 *
	 * @param request
	 * @param response
	 * @throws HTTPException
	 */
	public <T extends HTTPResponse<?>> T request(HTTPRequest<?> request, Class<T> responseClass) throws HTTPException {
		return this.request(request, -1, responseClass);
	}

	/**
	 * 发送一个同步的http请求
	 * 
	 * @param request
	 * @param timeout
	 * @param responseClass
	 * @return
	 * @throws HTTPException
	 */
	public <T extends HTTPResponse<?>> T request(HTTPRequest<?> request, int timeout, Class<T> responseClass) throws HTTPException {
		try {
			T response = responseClass.getConstructor(new Class[] {}).newInstance();
			RequestConfig config = null;
			if (timeout > 0) {
				config = RequestConfig.custom().setSocketTimeout(timeout).setConnectTimeout(timeout).build();
			}
			this.send(request, config, response);
			return response;
		} catch (Exception e) {
			throw new HTTPException(e);
		}
	}

	/**
	 * 发送一个异步的http请求，通过receiver回调结果
	 *
	 * @param request
	 * @param responseClass
	 * @param receiver
	 */
	public <Request extends HTTPRequest<?>, Response extends HTTPResponse<?>> void request(Request request, Class<Response> responseClass,
			ResponseReceivable<Request, Response> receiver) {
		this.request(request, -1, responseClass, receiver);
	}

	/**
	 * 发送一个异步的http请求，通过receiver回调结果
	 * 
	 * @param request
	 * @param timeout
	 * @param responseClass
	 * @param receiver
	 */
	public <Request extends HTTPRequest<?>, Response extends HTTPResponse<?>> void request(Request request, int timeout, Class<Response> responseClass,
			ResponseReceivable<Request, Response> receiver) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					Response response = HTTPConnection.this.request(request, timeout, responseClass);
					receiver.received(HTTPConnection.this, request, response);
				} catch (HTTPException e) {
					receiver.caughtException(HTTPConnection.this, e);
				}
			}
		}, "HTTP-Request-Thread").start();
	}

	@SuppressWarnings("deprecation")
	private void send(HTTPRequest<?> request, RequestConfig config, HTTPResponse<?> response) throws HTTPException {
		HttpRequestBase action = null;
		try {
			Method method = request.method();
			String url = request.url();
			if (request.redirectedUrl() != null) {
				url = request.redirectedUrl();
			}
			if (method == Method.Get) {
				action = new HttpGet(url);
			} else if (method == Method.Post) {
				action = new HttpPost(url);
			} else if (method == Method.Patch) {
				action = new HttpPatch(url);
			} else if (method == Method.Options) {
				action = new HttpOptions(url);
			}
			if (action == null) {
				throw new HttpException("不支持的Method: " + method.toString());
			}
			// RequestConfig requestConfig =
			// RequestConfig.custom().setSocketTimeout(5000).setConnectTimeout(5000).build();
			if (config != null) {
				action.setConfig(config);
			}
			byte[] body = request.encodedBody();
			if (body != null && action instanceof HttpEntityEnclosingRequestBase) {
				HttpEntityEnclosingRequestBase enclosing = (HttpEntityEnclosingRequestBase) action;
				if (request.gzipSupported()) {
					ByteArrayOutputStream buffer = new ByteArrayOutputStream(body.length);
					GZIPOutputStream gzip = new GZIPOutputStream(buffer);
					gzip.write(body);
					gzip.flush();
					gzip.finish();
					enclosing.setEntity(new ByteArrayEntity(buffer.toByteArray()));
					gzip.close();
					request.setHeader(HTTPProtocol.HeaderKey.ContentEncoding, ContentEncoding.GZip);
				} else {
					enclosing.setEntity(new ByteArrayEntity(body));
				}
			}
			Enumeration<String> keys = request.allHeaderKeys();
			while (keys.hasMoreElements()) {
				String key = keys.nextElement();
				String value = request.getHeaderValue(key);
				action.setHeader(key, value);
			}
			HttpResponse resp = this.client.execute(action);
			if (resp != null) {
				if (resp.getStatusLine().getStatusCode() == HTTPProtocol.Status.MoveTemporarily.code()
						|| resp.getStatusLine().getStatusCode() == HTTPProtocol.Status.TemporaryRedirect.code()) {
					String location = resp.getFirstHeader(HeaderKey.Location).getValue();
					request.redirect(location);
					action.abort();
					this.send(request, config, response);
				} else {
					response.respond(resp);
				}
			} else {
				throw new HTTPException("未收到任何http回应");
			}
		} catch (Exception e) {
			throw new HTTPException(e);
		} finally {
			if (action != null && !action.isAborted()) {
				action.abort();
			}
		}
	}

	public void close() {
		try {
			this.client.close();
			if (this.manager != null) {
				this.manager.closeExpiredConnections();
				// this.manager.closeIdleConnections(idleTimeout, tunit);
			}
		} catch (IOException e) {
		}
	}

	public CookieStore cookieStore() {
		return this.cookieStore;
	}

	public HTTPProxy proxy() {
		return this.proxy;
	}

	public PoolingHttpClientConnectionManager manager() {
		return this.manager;
	}

	public void addCookie(String name, String value, String domain, String path, Date expiration, boolean secure) {
		BasicClientCookie cookie = new BasicClientCookie(name, value);
		cookie.setDomain(domain);
		cookie.setPath(path);
		cookie.setExpiryDate(expiration);
		cookie.setSecure(secure);
		this.cookieStore().addCookie(cookie);
	}

	public Cookie findCookie(String name) {
		return this.findCookie(name, null, null);
	}

	public Cookie findCookie(String name, String domain, String path) {
		Cookie result = null;
		BasicCookieStore cs = (BasicCookieStore) this.cookieStore();
		if (cs != null) {
			List<Cookie> cookies = cs.getCookies();
			if (cookies != null) {
				for (int i = 0; i < cookies.size(); i++) {
					Cookie cookie = cookies.get(i);
					String d = cookie.getDomain();
					String p = cookie.getPath();
					String n = cookie.getName();
					if (n.equals(name)) {
						if (domain == null || (d != null && d.equals(domain))) {
							if (path == null || (p != null && d.equals(path))) {
								result = cookie;
								break;
							}
						}
					}
				}
			}
		}
		return result;
	}
}
